Skip to content

Triggers: support managing columns for data iteration#7541

Open
labkey-nicka wants to merge 15 commits intodevelopfrom
fb_trigger_columns
Open

Triggers: support managing columns for data iteration#7541
labkey-nicka wants to merge 15 commits intodevelopfrom
fb_trigger_columns

Conversation

@labkey-nicka
Copy link
Copy Markdown
Contributor

@labkey-nicka labkey-nicka commented Apr 2, 2026

Rationale

When query triggers are called within data iterators they are allowed to manipulate the data of the row that is being procured. However, if a trigger modifies the columns (i.e., the keys of the row map) it can result in data being mismanaged due to the columns in the row not aligning with what the data iterator specified.

This introduces the notion of "managed" columns to triggers. Triggers can, at the outset, declare which columns they are going to manage. Any managed columns are then picked up by the data iterator during initialization to ensure they are persisted correctly. To enforce this management more strictly, and to avoid mismanagement of null values, triggers are required to fulfill all managed column values by either clearing the value or setting the value. After the trigger has fired the resulting row will be validated and checked. The result is consistent enforcement and acknowledgement of triggers that manipulate the shape of the data.

Related Pull Requests

Changes

  • Declare TableInfo.getTriggerManagedColumns() to allow for data iterator to gather all managed columns
  • Add difference checks for non-managed columns added or removed by triggers
  • Support managed columns in server-side JS trigger scripts
  • Add utilities to ensure managed column values are set (setInsertManagedColumns() and setUpdateManagedColumns())
  • Add experimental feature flag which when enabled will disable the managed trigger columns feature

Tasks

  • Initial review @labkey-matthewb
  • Hook up server-side JS triggers
  • Only throw errors in development for non-data iterator triggers
  • Add experimental feature flag to disable
  • Optimize row checks
  • Needs automation
  • Code review @mbellew
  • Manual testing @XingY
  • Verify fix

Manual Testing notes (in progress)

  • Study dataset: the presence of a trigger script result in existing QCState erased using "Import with Update" (Not merge, which is a different known issue).
    • Unrelated to this branch: reproduces on develop
  • Study dataset: the presence of the following trigger script setup result in inability to update a dataset row from detail edit (non data iterator). The update page refreshes on submit, without error or success feedback, the data is not actually updated
    • Unrelated to this branch: reproduces on develop
function beforeUpdate(row, errors) {
    row['QC State'] = 'clean';
}

function managedColumns(row, errors) {
    return {
        update: ['QC State']
    }
}
  • Lists (but maybe other data type as well): if the trigger script provides an invalid value for the managed column, for example, a string value for an int field. The import fails with a "false" error msg.
    • Unrelated to this branch: reproduces on develop
  • Not sure if related to this branch (data integrity): trigger script only works for column name, not column label. However, if the trigger script uses label (such as 'Status' for 'SampleState'), then that results in existing SampleState being set to blank.
    • Unrelated to this branch: reproduces on develop
    • Seems like this is the same as this issue (current critical issue)
  • Trigger script doesn't work for parent input columns for sources and samples
    • Unrelated to this branch: reproduces on develop

@labkey-nicka labkey-nicka self-assigned this Apr 2, 2026
Copy link
Copy Markdown
Contributor

@labkey-matthewb labkey-matthewb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considerations for additional features (do we want to do now or never)?

  • Support different managed columns for insert/update
  • Support in JavaScript triggers

@labkey-nicka labkey-nicka modified the milestones: 26.04, 26.05 Apr 3, 2026
@labkey-nicka labkey-nicka marked this pull request as ready for review April 3, 2026 17:15
@labkey-nicka
Copy link
Copy Markdown
Contributor Author

Support different managed columns for insert/update
Support in JavaScript triggers

Implemented both of these.

if (errors.hasErrors())
break;

if (trackedRow != null)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add manageColumns check to this check? if (trackedRow != null && manageColumns)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we want to inform/check for changes regardless of the state of the manageColumns flag.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. LOG.warn()

Copy link
Copy Markdown
Contributor Author

@labkey-nicka labkey-nicka Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've revised this check to no longer apply to the non-data iterator configuration. We now will only check for managed columns when in a data iterator (and before is true and experimental flag is true). No more logging warnings as encountering this in a data iterator is always considered an error.

@labkey-matthewb labkey-matthewb self-requested a review April 7, 2026 18:26
Copy link
Copy Markdown
Contributor

@labkey-matthewb labkey-matthewb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the error checking, and merging in the values for the managed columns early is an interesting idea that probably greatly reduces the chance of losing data.

@labkey-matthewb labkey-matthewb self-requested a review April 7, 2026 18:31
Copy link
Copy Markdown
Contributor

@labkey-matthewb labkey-matthewb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One small suggestion. I like the strict error checking.

return;

if (oldRow == null)
throw new IllegalArgumentException("An existing record must be supplied for all UPDATE triggers");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there are no managed columns, can oldRow be null? It seems the caller all have oldRow as Nullable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The term "An existing record" is ambiguous here. oldRow and existingRecord are different.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to be consistent in how this is referred to in error messaging, not necessarily with the code name. Intentional.

*/
default void setInsertManagedColumns(
Map<String, Object> newRow,
@Nullable Map<String, Object> existingRecord,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

existingRecord doesn't seem actually used, is it meant to be passed in as oldRow? If not, the param could instead be hasExistingRecord for existingRecord == null

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I was intending to pass the existingRecord through. Done.

* @param oldRow The previous row for UPDATE and DELETE
* @param extraContext Optional additional bindings to set in the script's context when evaluating.
* @param existingRecord Optional existing record for the row, used for merge operation to differentiate new vs existing row
* @param manageColumns Whether to manage columns for the row.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This param confuses me with the experimental flag, consider renaming?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed this parameter and now compute manageColumns internally to the method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants